/**
 *  @file
 *  @author Stefan Frings
 */

#include "httpconnectionhandler.h"
#include "httpresponse.h"

using namespace stefanfrings;

HttpConnectionHandler::HttpConnectionHandler(QHash<QString, QVariant> settings, HttpRequestHandler *requestHandler,
                                             const QSslConfiguration *sslConfiguration)
  : QObject()
{
  Q_ASSERT(requestHandler != nullptr);
  this->settings = settings;
  this->requestHandler = requestHandler;
  this->sslConfiguration = sslConfiguration;
  currentRequest = nullptr;
  busy = false;

  // execute signals in a new thread
  thread = new QThread();
  thread->start();
  qDebug("HttpConnectionHandler (%p): thread started", static_cast<void *>(this));
  moveToThread(thread);
  readTimer.moveToThread(thread);
  readTimer.setSingleShot(true);

  // Create TCP or SSL socket
  createSocket();
  socket->moveToThread(thread);

  // Connect signals
  connect(socket, SIGNAL(readyRead()), SLOT(read()));
  connect(socket, SIGNAL(disconnected()), SLOT(disconnected()));
  connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout()));
  connect(thread, SIGNAL(finished()), this, SLOT(thread_done()));

  qDebug("HttpConnectionHandler (%p): constructed", static_cast<void *>(this));
}

void HttpConnectionHandler::thread_done()
{
  readTimer.stop();
  socket->close();
  delete socket;
  qDebug("HttpConnectionHandler (%p): thread stopped", static_cast<void *>(this));
}

HttpConnectionHandler::~HttpConnectionHandler()
{
  thread->quit();
  thread->wait();
  thread->deleteLater();
  qDebug("HttpConnectionHandler (%p): destroyed", static_cast<void *>(this));
}

void HttpConnectionHandler::createSocket()
{
  // If SSL is supported and configured, then create an instance of QSslSocket
    #ifndef QT_NO_OPENSSL
  if(sslConfiguration)
  {
    QSslSocket *sslSocket = new QSslSocket();
    sslSocket->setSslConfiguration(*sslConfiguration);
    socket = sslSocket;
    qDebug("HttpConnectionHandler (%p): SSL is enabled", static_cast<void *>(this));
    return;
  }
    #endif
  // else create an instance of QTcpSocket
  socket = new QTcpSocket();
}

void HttpConnectionHandler::handleConnection(tSocketDescriptor socketDescriptor)
{
  qDebug("HttpConnectionHandler (%p): handle new connection", static_cast<void *>(this));
  busy = true;
  Q_ASSERT(socket->isOpen() == false); // if not, then the handler is already busy

  // UGLY workaround - we need to clear writebuffer before reusing this socket
  // https://bugreports.qt-project.org/browse/QTBUG-28914
  socket->connectToHost("", 0);
  socket->abort();

  if(!socket->setSocketDescriptor(socketDescriptor))
  {
    qCritical("HttpConnectionHandler (%p): cannot initialize socket: %s",
              static_cast<void *>(this), qPrintable(socket->errorString()));
    return;
  }

    #ifndef QT_NO_OPENSSL
  // Switch on encryption, if SSL is configured
  if(sslConfiguration)
  {
    qDebug("HttpConnectionHandler (%p): Starting encryption", static_cast<void *>(this));
    (static_cast<QSslSocket *>(socket))->startServerEncryption();
  }
    #endif

  // Start timer for read timeout
  int readTimeout = settings.value("readTimeout", 10000).toInt();
  readTimer.start(readTimeout);
  // delete previous request
  delete currentRequest;
  currentRequest = nullptr;
}

bool HttpConnectionHandler::isBusy()
{
  return busy;
}

void HttpConnectionHandler::setBusy()
{
  this->busy = true;
}

void HttpConnectionHandler::readTimeout()
{
  qDebug("HttpConnectionHandler (%p): read timeout occured", static_cast<void *>(this));

  // Commented out because QWebView cannot handle this.
  // socket->write("HTTP/1.1 408 request timeout\r\nConnection: close\r\n\r\n408 request timeout\r\n");

  while(socket->bytesToWrite())
    socket->waitForBytesWritten();
  socket->disconnectFromHost();
  delete currentRequest;
  currentRequest = nullptr;
}

void HttpConnectionHandler::disconnected()
{
  qDebug("HttpConnectionHandler (%p): disconnected", static_cast<void *>(this));
  socket->close();
  readTimer.stop();
  busy = false;
}

void HttpConnectionHandler::read()
{
  // The loop adds support for HTTP pipelinig
  while(socket->bytesAvailable())
  {
        #ifdef SUPERVERBOSE
    qDebug("HttpConnectionHandler (%p): read input", static_cast<void *>(this));
        #endif

    // Create new HttpRequest object if necessary
    if(!currentRequest)
    {
      currentRequest = new HttpRequest(settings);
    }

    // Collect data for the request object
    while(socket->bytesAvailable() && currentRequest->getStatus() != HttpRequest::complete &&
          currentRequest->getStatus() != HttpRequest::abort)
    {
      currentRequest->readFromSocket(socket);
      if(currentRequest->getStatus() == HttpRequest::waitForBody)
      {
        // Restart timer for read timeout, otherwise it would
        // expire during large file uploads.
        int readTimeout = settings.value("readTimeout", 10000).toInt();
        readTimer.start(readTimeout);
      }
    }

    // If the request is aborted, return error message and close the connection
    if(currentRequest->getStatus() == HttpRequest::abort)
    {
      socket->write("HTTP/1.1 413 entity too large\r\nConnection: close\r\n\r\n413 Entity too large\r\n");
      while(socket->bytesToWrite())
        socket->waitForBytesWritten();
      socket->disconnectFromHost();
      delete currentRequest;
      currentRequest = nullptr;
      return;
    }

    // If the request is complete, let the request mapper dispatch it
    if(currentRequest->getStatus() == HttpRequest::complete)
    {
      readTimer.stop();
      qDebug("HttpConnectionHandler (%p): received request", static_cast<void *>(this));

      // Copy the Connection:close header to the response
      HttpResponse response(socket);
      bool closeConnection =
        QString::compare(currentRequest->getHeader("Connection"), "close", Qt::CaseInsensitive) == 0;
      if(closeConnection)
      {
        response.setHeader("Connection", "close");
      }
      // In case of HTTP 1.0 protocol add the Connection:close header.
      // This ensures that the HttpResponse does not activate chunked mode, which is not spported by HTTP 1.0.
      else
      {
        bool http1_0 = QString::compare(currentRequest->getVersion(), "HTTP/1.0", Qt::CaseInsensitive) == 0;
        if(http1_0)
        {
          closeConnection = true;
          response.setHeader("Connection", "close");
        }
      }

      // Call the request mapper
      try
      {
        requestHandler->service(*currentRequest, response);
      }
      catch(...)
      {
        qCritical("HttpConnectionHandler (%p): An uncatched exception occured in the request handler",
                  static_cast<void *>(this));
      }

      // Finalize sending the response if not already done
      if(!response.hasSentLastPart())
      {
        response.write(QByteArray(), true);
      }

      qDebug("HttpConnectionHandler (%p): finished request", static_cast<void *>(this));

      // Find out whether the connection must be closed
      if(!closeConnection)
      {
        // Maybe the request handler or mapper added a Connection:close header in the meantime
        bool closeResponse =
          QString::compare(response.getHeaders().value("Connection"), "close", Qt::CaseInsensitive) == 0;
        if(closeResponse == true)
        {
          closeConnection = true;
        }
        else
        {
          // If we have no Content-Length header and did not use chunked mode, then we have to close the
          // connection to tell the HTTP client that the end of the response has been reached.
          bool hasContentLength = response.getHeaders().contains("Content-Length");
          if(!hasContentLength)
          {
            bool hasChunkedMode = QString::compare(response.getHeaders().value(
                                                     "Transfer-Encoding"), "chunked", Qt::CaseInsensitive) == 0;
            if(!hasChunkedMode)
            {
              closeConnection = true;
            }
          }
        }
      }

      // Close the connection or prepare for the next request on the same connection.
      if(closeConnection)
      {
        while(socket->bytesToWrite())
          socket->waitForBytesWritten();
        socket->disconnectFromHost();
      }
      else
      {
        // Start timer for next request
        int readTimeout = settings.value("readTimeout", 10000).toInt();
        readTimer.start(readTimeout);
      }
      delete currentRequest;
      currentRequest = nullptr;
    }
  }
}
